/**
 * 本文件意图分解重构重力点脚本，在分解过程中学习重力点脚本中各种要点
 */

//构建页面fps状态检查
var stats = new Stats(),
    sts = stats.domElement.style;
sts.position = 'absolute';
sts.top = '0px';
sts.right = '0px';
document.body.appendChild(stats.domElement);

/*----====================================---- */
/*----===============矢量类================---- */
/*----====================================---- */
/**
 * 矢量类构造函数，用于计算速度、加速度、位置的基础类
 * @param {Number} x 当前坐标x值
 * @param {Number} y 当前租表y值
 */
function Vector(x, y) {
    this.x = x || 0;
    this.y = y || 0;
}
/**
 * 计算两个矢量对象的x、y值之和，构建新的矢量
 * @param {Vecotr} a 矢量a
 * @param {Vecotr} b 矢量b
 */
Vector.add = function(a, b) {
    return new Vector(a.x + b.x, a.y + b.y);
}
/**
 * 计算两个矢量对象的x、y值之差，构建新的矢量
 * @param {Vector} a 矢量a
 * @param {Vector} b 矢量b
 */
Vector.sub = function(a, b) {
    return new Vector(a.x - b.x, a.y - b.y);
}
/**
 * 将指定Vector实例x、y值扩大指定倍数，返回新的引用
 * @param {Vecotr} v 被操作的Vector实例
 * @param {Number} s 要扩大的倍数
 */
Vector.scale = function(v, s) {
    return v.clone().scale(s);
}
/**
 * 返回一个x、y取值范围为[-1, 1)的Vector
 */
Vector.random = function() {
    return new Vector(
        Math.random() * 2 - 1,
        Math.random() * 2 - 1
    )
}
//矢量类原型对象
Vector.prototype = {
    //设置值，若一参为对象，则修改参数值
    set: function(x, y) {
        if(typeof x === 'object') {
            y = x.y;
            x = x.x;
        }
        this.x = x || 0;
        this.y = y || 0;
        return this;
    },
    //加上一个矢量
    add: function(v) {
        this.x += v.x;
        this.y += v.y;
        return this;
    },
    //减去一个矢量
    sub:function(v) {
        this.x -= v.x;
        this.y -= v.y;
        return this;
    },
    //x、y值扩大s（Number）倍，返回自身
    scale: function(s) {
        this.x *= s;
        this.y *= s;
        return this;
    },
    //当前矢量点在坐标轴内距原点距离
    length: function() {
        return Math.sqrt(this.x * this.x + this.y * this.y);
    },
    //当前矢量点坐标值的平方和
    lengthSq: function() {
        return this.x * this.x + this.y * this.y;
    },
    //坐标值三角函数化。当前矢量点与原点连线和x轴夹角的余弦(x)、正弦(y)值
    normalize: function() {
        var m = Math.sqrt(this.x * this.x + this.y * this.y);
        if(m) {
            this.x /= m; //夹角余弦值
            this.y /= m; //夹角正弦值
        }
        //可直接用于矢量分解
        return this;
    },
    //当前矢量点与x轴夹角的度数
    angle: function(v) {
        return Math.atan2(this.x, this.y);
    },
    //当前矢量点与原点连线 和 指定矢量点与原点之间连线的夹角度数
    angleTo: function(v) {
        var dx = v.x - this.x,
            dy = v.y - this.y;
        return Math.atan2(dx, dy);
    },
    //当前矢量点与指定矢量点之间距离
    distanceTo: function(v) {
        var dx = v.x - this.x,
            dy = v.y - this.y;
        return Math.sqrt(dx * dx + dy * dy);
    },
    //克隆自身，返回新的引用
    clone: function() {
        return new Vector(this.x, this.y);
    }
}


/*----====================================---- */
/*----===============重力点类==============---- */
/*----====================================---- */
/**
 * 重力点类的构造函数
 * @param {Number} x 重力点横坐标
 * @param {Number} y 重力点纵坐标
 * @param {Number} radius 重力点初始半径 
 * @param {Object} targets 包含 particles和gravities两个属性，表示当前重力点作用的例子和其他重力点数组
 */
function GravityPoint(x, y, radius, targets) {
    Vector.call(this, x, y);
    this.radius = radius || 0;
    this.currentRadius = radius * .5;
    this._targets = {
        particles: targets.particles || [],
        gravities: targets.gravities || []
    };
    this._speed = new Vector();
}
//重力点类的静态属性
GravityPoint.RADIUS_LIMIT = 65;
GravityPoint.interferenceToPoint = true; //标识重力点之间是否相互作用
//重力点类原型对象
GravityPoint.prototype = (function(o){
    var s = new Vector(0, 0), p;
    for(p in o) s[p] = o[p];
    return s;
})({
    gravity: .05, //引力加速度
    isMouseOver: false, //鼠标是否进入到当前重力点
    dragging: false, //是否处于拖动状态
    destroyed: false, //是否在下一帧被销毁
    _easeRadius:   0, //半径缓动
    _dragDistance: null, //重力点内按下位置到圆心距离(分解为x、y两个值)
    _collapsing:   false, //是否在下一帧坍塌
    //碰撞检测
    hitTest: function(p) {
        return this.distanceTo(p) < this.radius;
    },
    //开始脱动处理函数
    startDrag: function(startPoint) {
        this._dragDistance = Vector.sub(startPoint, this);
        this.dragging = true;
    },
    //拖动处理函数，不断地修改当前重力点的坐标
    drag: function(dragPoint) {
        this.x = dragPoint.x - this._dragDistance.x;
        this.y = dragPoint.y - this._dragDistance.y;
    },
    //拖动结束时处理函数
    endDrag: function() {
        this._dragDistance = null;
        this.dragging = false;
    },
    //速度变化
    addSpeed: function(d) {
        this._speed = this._speed.add(d);
    },
    //坍塌
    collapse: function() {
        this.currentRadius *= 1.75;
        this._collapsing = true;
    },
    //渲染数据
    rander: function(ctx) {
        if(this.destroyed) return;

        //对粒子点施加作用
        var particles = this._targets.particles,
            i;
        for(i = 0; i < particles.length; i += 1) {
            particles[i].addSpeed(Vector.sub(this, particles[i]).normalize().scale(this.gravity));
        }

        //这一段代码有点精髓，通过_easeRadius作为中间量，不断修改currenRadius
        //currenRadius的改变又引发_easeRadius的变化，
        //最终形成的效果是重力点的弹性缩放效果，最终currentRadius等于radius。
        this._easeRadius = (this._easeRadius + (this.radius - this.currentRadius) * .07) * .95;
        this.currentRadius += this._easeRadius;
        if(this.currentRadius < 0) this.currentRadius = 0;
        
        //判断是否坍塌，是否删除
        if(this._collapsing) {
            this.radius *= .75;
            if(this.currentRadius < 1) this.destroyed = true;
            this._draw(ctx);
            return;
        }

        //处理重力点的合并坍塌
        var gravities = this._targets.gravities,
            g, absorp,
            area = this.radius * this.radius * Math.PI, garea;
        for(i = 0; i < gravities.length; i += 1) {
            g = gravities[i];
            //跳过自身和处于拖动状态的重力点
            if(g === this || g.dragging) continue;
            //合并条件
            if(
                (this.currentRadius >= this.radius) &&
                (this.distanceTo(g) < (this.currentRadius + g.radius) * .85)
            ) {
                g.destroyed = true; //被动点坍塌
                this.gravity += g.gravity; //主动点引力增加

                absorp = Vector.sub(g, this).scale(g.radius / this.radius * .5);
                this.addSpeed(absorp); //主动点加速

                //根据面积调整主动点半径
                garea = g.radius * g.radius * Math.PI;
                this.currentRadius = Math.sqrt((garea + area) * 3 / Math.PI);
                this.radius = Math.sqrt((garea + area) / Math.PI);
            }
            //分解主动点加速度，被动重力点加速
            g.addSpeed(Vector.sub(this, g).normalize().scale(this.gravity));
        }

        //移动重力点
        if(GravityPoint.interferenceToPoint && !this.dragging) {
            this.add(this._speed);
        }
        this._speed = new Vector(); //每次重置速度值

        //检查半径是否超过限制值，超过就坍塌
        if(this.currentRadius > GravityPoint.RADIUS_LIMIT) this.collapse();

        this._draw(ctx);
    },
    //绘制函数
    _draw: function(ctx) {
        var grd, r;
        ctx.save();
        grd = ctx.createRadialGradient(this.x, this.y, this.radius, this.x, this.y, this.radius * 5);
        grd.addColorStop(0, 'rgba(0, 0, 0, .1)');
        grd.addColorStop(1, 'rgba(0, 0, 0, 0)');
        ctx.fillStyle = grd;
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
        ctx.closePath();
        ctx.fill();

        var r = Math.random() * this.currentRadius * .7 + this.currentRadius * .3;
        grd = ctx.createRadialGradient(this.x, this.y, r, this.x, this.y, this.currentRadius);
        grd.addColorStop(0, 'rgba(0, 0, 0, 1)');
        grd.addColorStop(1, Math.random() < .2 ? 'rgba(255, 196, 0, 0.15)' : 'rgba(103, 181, 191, 0.75)');
        ctx.fillStyle = grd;
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.currentRadius, 0, Math.PI * 2, false);
        ctx.closePath();
        ctx.fill();
    }
})


/*----====================================---- */
/*----===============粒子点类==============---- */
/*----====================================---- */
/**
 * 粒子类构造函数
 * @param {Number} x 粒子位置横坐标
 * @param {Number} y 粒子位置纵坐标
 * @param {Number} radius 粒子初始半径
 */
function Particle(x, y, radius) {
    Vector.call(this, x, y);
    this.radius = radius || 0;
    this._latest = new Vector();
    this._speed = new Vector();
}
//粒子类原型
Particle.prototype = (function(o){
    var s = new Vector(), p;
    for(p in o) s[p] = o[p];
    return s;
})({
    //加速
    addSpeed: function(d) {
        this._speed.add(d);
    },
    //移动
    update: function() {
        this._latest.set(this);
        this.add(this._speed);
    }
});

/*----====================================---- */
/*----===============初始化==============---- */
/*----====================================---- */
(function(){
    //配置
    var BACKGROUND_COLOR = 'rgba(11, 51, 56, 1)',
        PARTICLE_RADIUS = 1,
        G_POINT_RADIUS = 10,
        G_POINT_RADIUS_LIMITS = 65;

    //变量
    var canvas = document.getElementById('c'),
        context = canvas.getContext('2d'),
        bufferCvs = document.createElement('canvas'),
        bufferCtx = bufferCvs.getContext('2d'),
        screenWidth, screenHeight, mouse = new Vector(),
        gravities = [],
        particles = [],
        grad,
        contral = {particleNum: 100}; //粒子总数
    /**
     * 向粒子数组里添加粒子对象
     * @param {Number} num 要添加的粒子总数
     */
    function addParticles(num) {
        var i, p;
        for(i = 0; i < num; i += 1) {
            p = new Particle(
                Math.floor(Math.random() * screenWidth),
                Math.floor(Math.random() * screenHeight),
                PARTICLE_RADIUS
            );
            //初始化速度
            p.addSpeed(Vector.random());
            //加入粒子数组
            particles.push(p);
        }
    }

    /**
     * window对象onresize事件的处理函数
     */
    function resize() {
        screenWidth = canvas.width = window.innerWidth;
        screenHeight = canvas.height = window.innerHeight;
        bufferCvs.width = screenWidth;
        bufferCvs.height = screenHeight;
        //中点坐标
        var cx = screenWidth / 2,
            cy = screenHeight / 2;
        grad = context.createRadialGradient(cx, cy, 0, cx, cy, Math.sqrt(cx * cx + cy * cy));
        grad.addColorStop(0, 'rgba(0, 0, 0, 0)');
        grad.addColorStop(1, 'rgba(0, 0, 0, .35)');
    }

    /**
     * canvas对象下，鼠标按下事件处理函数
     */
    function mouseDown(e) {
        //遍历已有重力点，修改拖动状态
        for(var i = gravities.length - 1; i >= 0; i--) {
            if(gravities[i].isMouseOver) {
                gravities[i].startDrag(mouse);
                return;
            }
        }
        gravities.push(new GravityPoint(
            e.clientX, e.clientY, G_POINT_RADIUS, {
                particles: particles,
                gravities: gravities
            }
        ))
    }

    /**
     * canvas对象，鼠标移动事件处理函数
     */
    function mouseMove(e) {
        mouse.set(e.clientX, e.clientY);
        var i, g, hit = false;
        for(i = gravities.length - 1; i >= 0; i -= 1) {
            g = gravities[i];
            if((!hit && g.hitTest(mouse)) || g.dragging) {
                g.isMouseOver = hit = true;
            } else {
                g.isMouseOver = false;
            }
        }
        canvas.style.cursor = hit ? 'pointer' : 'default';
    }

    /**
     * canvas对象下，鼠标抬起事件处理函数
     */
    function mouseUp(e) {
        for(var i = gravities.length - 1; i >= 0; i -= 1) {
            if(gravities[i].dragging) {
                gravities[i].endDrag();
                return;
            }
        }
    }

    /**
     * 双击重力点处理函数
     */
    function doubleClick() {
        for(var i = gravities.length - 1; i >= 0; i++) {
            if(gravities[i].isMouseOver) {
                gravities[i].collapse();
                break;
            }
        }
    }

    //事件绑定
    window.addEventListener('resize', resize, false);
    canvas.addEventListener('mousedown', mouseDown, false);
    canvas.addEventListener('mousemove', mouseMove, false);
    canvas.addEventListener('mouseup', mouseUp, false);
    canvas.addEventListener('dblclick', doubleClick, false);
    resize();
    addParticles(contral.particleNum);

    //动画函数
    function animate() {
        context.save();
        context.fillStyle = BACKGROUND_COLOR;
        context.fillRect(0, 0, canvas.width, canvas.height);
        context.fillStyle = grad;
        context.fillRect(0, 0, canvas.width, canvas.height);
        context.restore();

        var i, len, g, p;
        for(i = 0, len = gravities.length; i < len; i++) {
            g = gravities[i];
            if(g.dragging) g.drag(mouse);
            g.rander(context);
            if(g.destroyed) {
                gravities.splice(i, 1);
                len--;
                i--;
            }
        }

        //内存绘制粒子遮罩图层
        bufferCtx.save();
        bufferCtx.globalCompositeOperation = 'destination-out';
        bufferCtx.globalAlpha = .35;
        bufferCtx.fillRect(0, 0, screenWidth, screenHeight);
        bufferCtx.restore();

        //内存绘制粒子点
        len = particles.length;
        bufferCtx.save();
        bufferCtx.fillStyle = bufferCtx.strokeStyle = '#fff';
        bufferCtx.lineCap = bufferCtx.lineJoin = 'round';
        bufferCtx.lineWidth = PARTICLE_RADIUS * 2;
        bufferCtx.beginPath();
        for(i = len - 1; i >= 0; i--) {
            p = particles[i];
            p.update();
            bufferCtx.moveTo(p.x, p.y);
            bufferCtx.lineTo(p._latest.x, p._latest.y);
        }
        bufferCtx.stroke();

        context.drawImage(bufferCvs, 0, 0);

        requestAnimationFrame(animate);
        stats.update();
    }
    animate();

})()